using System;
using System.Data;
using System.IO;
using System.Text;
using Agile.Common;
using Agile.Common.UI;
using GeneratorCustomization;

namespace Agile.Genie.Generators
{
    /// <summary>
    /// Generates the NAnt master build file for all build files in a specific directory 
    /// (including sub directories).
    /// </summary>
    [GUIDetails("Master Build File")]
    public class MasterBuildFileGenerator : Generator
    {
        private bool includeDirInFileName;
        private bool includeDatabaseBuild;

        #region Constructors and Factories

        /// <summary>
        /// Constructor
        /// </summary>
        private MasterBuildFileGenerator(DirectoryInfo baseDirectory, bool includeDirInFileName, bool includeDatabaseBuild)
        {
            _baseDirectory = baseDirectory;
            this.includeDirInFileName = includeDirInFileName;
            this.includeDatabaseBuild = includeDatabaseBuild;
        }

        /// <summary>
        /// Instantiate a new Master Build file generator for the given base directory.
        /// </summary>
        /// <remarks></remarks>
        /// <returns>Returns the instantiated generator.</returns>
        public static MasterBuildFileGenerator Build(DirectoryInfo baseDirectory, bool includeDirInFileName, bool includeDatabaseBuild)
        {
            return new MasterBuildFileGenerator(baseDirectory, includeDirInFileName, includeDatabaseBuild);
        }

        #endregion

        private readonly DirectoryInfo _baseDirectory;

        /// <summary>
        /// Gets the Base Directory that we are generating the master build file for.
        /// </summary>
        public DirectoryInfo BaseDirectory
        {
            get { return _baseDirectory; }
        }

        #region Generation Code

        private DataTable serverTestingProjects;

        /// <summary>
        /// Wrap the given code (which will be the all of the other generated code)
        /// in the project element.
        /// </summary>
        /// <param name="codeToWrap">Code to wrap.</param>
        /// <param name="projectName">Name of the project, e.g. Generators</param>
        /// <returns></returns>
        public string WrapInProjectElement(string codeToWrap, string projectName)
        {
            return string.Format(@"<project name=""{1}"" default=""go"" basedir=""."">
{0}
</project>"
                                 , codeToWrap
                                 , projectName);
        }

        /// <summary>
        /// Returns true if the tests for the project test the server.
        /// </summary>
        /// <remarks>COPIED IN BuildFileGenerator</remarks>
        /// <returns></returns>
        private bool ProjectIsServerTesting(AgileFileInfo buildFile)
        {
            if (serverTestingProjects == null)
                serverTestingProjects = GeneratorsData.GetDataTable("ServerTestingProjects");

            foreach (DataRow row in serverTestingProjects.Rows)
            {
                if (row["ProjectName"].ToString() == buildFile.NameExcludingExtension)
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Generate code for the 'go' target.
        /// </summary>
        /// <returns></returns>
        public string GenerateTargetGo()
        {
            var fileList = new StringBuilder();
            foreach (AgileFileInfo buildFile in BuildFiles)
            {
                if (buildFile.Name == "DBUpdater.build")
                    continue;

                if (ProjectIsServerTesting(buildFile))
                {
                    fileList.Append(string.Format(@"
            , BuildOnly{0}
            , TestOnly{0}"
                                                  , buildFile.NameExcludingExtension
                                        ));
                }
                else
                {
                    fileList.Append(string.Format(@"
        , Build{0}"
                                                  , buildFile.NameExcludingExtension
                                        ));
                }
            }

            string goTarget = string.Format(
                @"
    <!-- TARGET: go
	     This lists everything that needs to be built.
	     The order is important!
	
	  -->
    <target name=""go"" depends=""
    OnStart 
    , ClearTests {1}

{0}
"" >
    </target>"
                , fileList
                , includeDatabaseBuild ? GetBuildAndRunDatabaseBuilderText : string.Empty
                );

            return goTarget;

            // Then for every build file in the directory and its subdirectories
            // Add 
        }

        private string GetBuildAndRunDatabaseBuilderText
        {
            get
            {
                return @"
    , BuildDBUpdate.Cmd
    , RunDBUpdater";
            }
        }

        /// <summary>
        /// Generates the BuildOnly target.
        /// </summary>
        /// <returns></returns>
        public string GenerateBuildOnly()
        {
            var fileList = new StringBuilder();
            foreach (AgileFileInfo buildFile in BuildFiles)
            {
                if (buildFile.Name == "DBUpdater.build")
                    continue;

                fileList.Append(string.Format(@"
        , BuildOnly{0}"
                                              , buildFile.NameExcludingExtension
                                    ));
            }

            string buildOnlyTarget = string.Format(
                @"
    <!-- TARGET: BuildOnly 
        Builds all of the listed projects without running any tests!
    -->
    <target name=""BuildOnly"" depends=""

{0} "" >
        <property name=""message"" value=""Projects successfully built WITHOUT ANY TESTING!""/>
        <call target=""showProgress"" />
    </target>
"
                , Strings.RemoveFirstInstanceOf(",", fileList.ToString())
                );
            return buildOnlyTarget;
        }

        /// <summary>
        /// Generates the build and BuildOnly targets for each project (that has a build file).
        /// </summary>
        /// <returns>Exactly what is shown in the example for each project that
        /// has a build file.</returns>
        public string GenerateProjectTargets()
        {
            var targets = new StringBuilder(GenerateRunDatabaseUpdate());
            foreach (AgileFileInfo buildFile in BuildFiles)
            {
                targets.Append(
                    string.Format(
                        @"
    <!-- TARGET: Build{0} -->
    <target name=""Build{0}"" description=""Building {0}"">
        <property name=""message"" value=""Build {0}""/>	
        <call target=""showProgress"" />
        <nant buildfile=""{1}"" target=""go"" inheritall=""true"" verbose=""true"" failonerror=""true"" />
    </target>
    <!-- TARGET: BuildOnly{0} -->
    <target name=""BuildOnly{0}"" description=""Building {0}"">
        <property name=""message"" value=""BuildOnly {0}""/>	
        <call target=""showProgress"" />
        <nant buildfile=""{1}"" target=""build"" inheritall=""true"" verbose=""true"" failonerror=""true"" />
    </target>
"
                        , buildFile.NameExcludingExtension
                        , BuildFileLocation(buildFile)));

                if (ProjectIsServerTesting(buildFile))
                {
                    targets.Append(
                        string.Format(
                            @"
    <!-- TARGET: TestOnly{0} -->
    <target name=""TestOnly{0}"" description=""Testing {0}"">
        <property name=""message"" value=""TestOnly {0}""/>	
        <call target=""showProgress"" />
        <nant buildfile=""{1}"" target=""test"" inheritall=""true"" verbose=""true"" failonerror=""false"" />
    </target>
"
                            , buildFile.NameExcludingExtension
                            , BuildFileLocation(buildFile)));
                }
            }
            return targets.ToString();
        }


        /// <summary>
        /// Returns the build file location as a string for the master build.
        /// i.e. starts with '${project::get-base-directory()}'
        /// </summary>
        /// <param name="buildFile"></param>
        /// <returns></returns>
        private string BuildFileLocation(AgileFileInfo buildFile)
        {
            var location = new StringBuilder("${project::get-base-directory()}");

//			// Handle differently for web files
//			if(buildFile.NameExcludingExtension.ToLower().EndsWith(".web"))
//				location.Append("\\" + projectNameBit.Substring(0, buildFile.NameExcludingExtension.Length - 4));

            location.Append(string.Format(@"{0}\{1}"
                                          , buildFile.FileInfo.DirectoryName.Replace(BaseDirectory.FullName, string.Empty)
                                          , buildFile.FileInfo.Name));
            return location.ToString();
        }

        /// <summary>
        /// Generates the OnStart section of the master build file.
        /// </summary>
        public string GenerateOnStart()
        {
            return
                @"
    <target name=""OnStart"" description=""Called when build starts"">
        <echo>Renaming the log file to 'PreviousBuildLog.txt'.</echo>
        <move file=""BuildLog.txt"" tofile=""PreviousBuildLog.txt"" overwrite=""true"" failonerror=""false""/>
        <property name=""message"" value=""OnStart""/>
        <call target=""showProgress"" />
        <property name=""nant.onsuccess"" value=""OnSuccess"" />
        <property name=""nant.onfailure"" value=""OnFailure"" />
    </target>
";
        }

        /// <summary>
        /// Generates the ClearTests section of the master build file.
        /// </summary>
        public string GenerateClearTests()
        {
            return
                @"
    <!-- TARGET: ClearTests
    Moves all existing test results to the Previous results directory.
	NOTE: All test results need to be copied to C:\Build\TestResults (Or whatever your RootDrive is)
	  -->
    <target name=""ClearTests"" description=""Clear test results"">
        <property name=""TestResultsDir"" value =""${directory::get-current-directory()}\Build\TestResults"" />
        <property name=""message"" value=""ClearTests""/>
        <property name=""DateDirectory"" value=""${TestResultsDir}\RunDate ${datetime::get-day(datetime::now())}.${datetime::get-month(datetime::now())}.${datetime::get-year(datetime::now())}"" />
        <property name=""TimeDirectory"" value=""RunTime ${datetime::get-hour(datetime::now())}.${datetime::get-minute(datetime::now())}"" />
        <property name=""PreviousResultsDirectory"" value=""${DateDirectory}\${TimeDirectory}"" />
        <call target=""showProgress"" />

        <echo message=""Moving previous test results to ${PreviousResultsDirectory}"" />
        <move todir=""${PreviousResultsDirectory}"" overwrite=""true"" failonerror=""false"">
          <fileset>
            <include name=""${TestResultsDir}\*.xml""/>
          </fileset>
        </move>
        <mkdir dir=""${TestResultsDir}"" failonerror=""false"" />
    </target>
";
        }

        /// <summary>
        /// Generates the ShowProgress section of the master build file.
        /// </summary>
        /// <returns>Exactly what is shown in the example.</returns>
        public string GenerateRunDatabaseUpdate()
        {
            return
                string.Format(
                    @"
    
    <!-- TARGET: RunDBUpdater
    	Runs the DB Update program which runs all change scripts.
    	-->
    <target name=""RunDBUpdater"" description=""Running DBUpdater"">
        <property name=""message"" value=""Running DB updater...""/>
        <echo message=""PROGRESS: ${{message}}""/>
        <sysinfo/>
        <exec 
            program=""${{project::get-base-directory()}}\DBUpdater\DBUpdate.Cmd\bin\Release\DBUpdate.exe""
            basedir="".""
            commandline=""""
            output=""results.txt""
            workingdir="".""
            failonerror=""true""/>
    </target>
");
        }


        /// <summary>
        /// Generates the ShowProgress section of the master build file.
        /// </summary>
        /// <example>
        /// NOTE: Add a less than symbol at the start of each line in the example.
        /// <p>
        ///     !-- TARGET: progress
        ///        Adds the current progress the BuildLog
        ///     -->
        ///         target name=""showProgress"" description=""Show progress"">
        ///         echo message=""PROGRESS: ${message}""/>
        ///     sysinfo/>
        ///     exec 
        ///         program=""C:\Windows\system32\cmd.exe""
        ///         basedir="".""
        ///         commandline=""/c echo %date% %time% ${message} >>BuildLog.txt""
        ///         output=""results.txt""
        ///         workingdir="".""
        ///         failonerror=""false""/>
        ///     /target>
        /// </p>
        /// </example>
        /// <returns>Exactly what is shown in the example.</returns>
        public string GenerateShowProgress()
        {
            string windowsDirectory = "WINNT";
            return
                string.Format(
                    @"
    <!-- TARGET: progress
    	Adds the current progress the BuildLog
    	-->
    <target name=""showProgress"" description=""Show progress"">
        <echo message=""PROGRESS: ${{message}}""/>
        <sysinfo/>
        <exec 
            program=""${{environment::get-variable('ComSpec')}}""
            basedir="".""
            commandline=""/c echo %date% %time% ${{message}} >>BuildLog.txt""
            output=""results.txt""
            workingdir="".""
            failonerror=""false""/>
    </target>
"
                    , windowsDirectory);
        }

        /// <summary>
        /// Generates the Success and Failure section of the master build file.
        /// </summary>
        /// <example>
        /// NOTE: Add a less than symbol at the start of each line in the example.
        /// <p>
        ///     target name="OnSuccess" description="Called when build succeeds">
        ///         property name="message" value="OnSuccess"/>
        ///         call target="showProgress" />
        ///     /target>
        ///
        ///     target name="OnFailure" description="Called when build fails">
        ///         property name="message" value="OnFailure"/>
        ///         call target="showProgress" />
        ///     /target>
        /// </p>
        /// </example>
        /// <returns>Exactly what is shown in the example.</returns>
        public string GenerateSuccessFailure()
        {
            return
                @"
    <target name=""OnSuccess"" description=""Called when build succeeds"">
        <property name=""message"" value=""OnSuccess""/>
        <call target=""showProgress"" />
    </target>

    <target name=""OnFailure"" description=""Called when build fails"">
        <property name=""message"" value=""OnFailure""/>
        <call target=""showProgress"" />
    </target>
";
        }

        #endregion

        #region Build Files

        private AgileFileInfoCollection _buildFiles;
        private AgileFileInfoCollection _ordered;
        private DataTable projectsExcludedFromBuild;

        /// <summary>
        /// Gets the collection of build files that are in the
        /// directory and its subdirectories.
        /// </summary>
        private AgileFileInfoCollection BuildFiles
        {
            get
            {
                if (_buildFiles == null)
                {
                    GetBuildFiles();
                    PutBuildFilesInRightOrder();
                }
                return _buildFiles;
            }
        }

        private bool IsProjectExcluded(string projectName)
        {
            foreach (DataRow row in projectsExcludedFromBuild.Rows)
            {
                if (row["ProjectName"].ToString() == projectName)
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Puts the build files in order of lowest level dependency.
        /// </summary>
        /// <remarks>The order that build files are run is important (mainly from a time saving
        /// perspective, no point building all other projects and then running common,
        /// find a bug and have to rebuild everything again...).
        /// <p>This looks at the each build file and the dependency list
        /// that has been put in them, then puts them in the correct order.
        /// i.e. Agile.Common will be first...</p></remarks>
        private void PutBuildFilesInRightOrder()
        {
            _ordered = AgileFileInfoCollection.Build();
            int lastCount = -1;

            if (projectsExcludedFromBuild == null)
                projectsExcludedFromBuild = GeneratorsData.GetDataTable("ProjectsExcludedFromBuild");


            while (_buildFiles.Count > 0)
            {
                // Stops an infinite loop some files are not getting added ...TODO: need to log
                if (lastCount == _buildFiles.Count)
                {
                    _ordered.AddRange(_buildFiles);
                    _buildFiles.Clear();
                }
                lastCount = _buildFiles.Count;

                // Add each file, as long as its referenced projects build files are already there.
                for (int i = _buildFiles.Count - 1; i > -1; i--)
                {
                    AgileFileInfo buildFile = _buildFiles[i];
                    if (IsProjectExcluded(buildFile.NameExcludingExtension))
                    {
                        _buildFiles.RemoveAt(i);
                        continue;
                    }

                    if (AreAllReferencedProjectsInOrderedList(buildFile))
                    {
                        _ordered.Add(buildFile);
                        _buildFiles.RemoveAt(i);
                    }
                }
            }
            // Reset Build files to the new ordered list.
            _buildFiles = _ordered;
        }

        /// <summary>
        /// Returns true if the build file for all referenced projects are already 
        /// in the new Ordered build file list.
        /// </summary>
        /// <param name="buildFile">Build file to check.</param>
        /// <returns></returns>
        private bool AreAllReferencedProjectsInOrderedList(AgileFileInfo buildFile)
        {
            string[] referencedProjects = GetReferencedProjects(buildFile);
            // If the first element is empty, there are no references
            if (referencedProjects[0].Trim() == string.Empty)
                return true;

            // Otherwise check all references.
            foreach (string referenced in referencedProjects)
            {
                if (IsProjectExcluded(referenced))
                    continue;
                if (!IsReferencedProjectInOrderedList(referenced.Trim()))
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Gets the referenced projects list from the given build file.
        /// </summary>
        /// <param name="buildFile"></param>
        /// <returns></returns>
        private string[] GetReferencedProjects(AgileFileInfo buildFile)
        {
            string fileContents = File.ReadAllText(buildFile.FileInfo.FullName);

            int startIndex = fileContents.IndexOf(BuildFileGenerator.ProjectDependencyString)
                             + BuildFileGenerator.ProjectDependencyString.Length;
            int endIndex = fileContents.IndexOf("-->", startIndex);
            string projectDependencies = fileContents.Substring(startIndex, endIndex - startIndex);
            return projectDependencies.Replace("\r\n", string.Empty).Split(char.Parse(","));
        }

        /// <summary>
        /// Returns true if the given referenced project is in the new Ordered collection of build files.
        /// </summary>
        /// <param name="referencedProject">Name of the referenced project.</param>
        /// <remarks>Checks for the project name and the projects Assembly name, because if the project
        /// is referenced it will be the project name but if the dll is referenced it will be the 
        /// assembly name.</remarks>
        /// <returns></returns>
        private bool IsReferencedProjectInOrderedList(string referencedProject)
        {
            ArgumentValidation.CheckForNullReference(referencedProject, "referencedProject");
            if (referencedProject == string.Empty)
                throw new ApplicationException("Referenced project cannot be an empty string.");

            foreach (AgileFileInfo buildFile in _ordered)
            {
                if ((buildFile.NameExcludingExtension.Equals(referencedProject)))
                    //|| (buildFile.NameExcludingExtension.Equals(referencedProject.AssemblyName))
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Finds all build files in the directory and subdirectories
        /// and adds them into BuildFiles.
        /// </summary>
        private void GetBuildFiles()
        {
            _buildFiles = AgileFileInfoCollection.Build();
            Files.LoopThroughSubDirectories(BaseDirectory, GoThroughFiles, ".svn", "_svn");
        }

        /// <summary>
        /// Go through all of the files in the directory
        /// </summary>
        /// <param name="directory"></param>
        private void GoThroughFiles(DirectoryInfo directory)
        {
            Files.GoThroughFilesIn(directory, AddIfBuildFile);
        }

        /// <summary>
        /// Check if the file is a build file, add it to the BuildFiles collection if it is
        /// </summary>
        /// <param name="file">The file to check and add if it is a build file.</param>
        private void AddIfBuildFile(FileInfo file)
        {
            if (Files.IsTheRightFileType(file, "build")
                && (!file.Name.Equals(FullFileName, StringComparison.InvariantCultureIgnoreCase)))
            {
                BuildFiles.Add(AgileFileInfo.Build(file));
            }
        }

        #endregion

        #region Overrides

        /// <summary>
        /// Returns an empty string (which is what we want, we want the master.build file to be generated in the root directory)
        /// </summary>
        protected override string FileGenerationDirectoryDetails
        {
            get { return string.Empty; }
        }

        /// <summary>
        /// Gets the base directory for file generation.
        /// </summary>
        /// <remarks>i.e. The base directory that the file will be generated in.</remarks>
        public override DirectoryInfo FileGenerationDirectory
        {
            get { return _baseDirectory; }
        }

        /// <summary>
        /// Gets the description of the purpose of the file.
        /// </summary>
        protected override string FilePurpose
        {
            get { return @"	*** Master Build File ***
	Builds all projects that have a build file."; }
        }

        /// <summary>
        /// Gets a string containing additional notes about the files contents.
        /// </summary>
        protected override string FileNotes
        {
            get { return string.Empty; }
        }


        /// <summary>
        /// Gets the file extension of the file that is to be created.
        /// </summary>
        /// <example>.sql</example>
        public override string FileExtension
        {
            get { return ".build"; }
        }

        private string fileName = "Master";
        /// <summary>
        /// Gets the file name of the file that is to be created, not including the file extension.
        /// </summary>
        /// <remarks>For the master build this will always be 'master.build'</remarks>
        /// <example>master.build</example>
        public override string FileName
        {
            get { return string.Format("{0}{1}"
                , includeDirInFileName ? FileGenerationDirectory.Name : string.Empty
                , fileName); }
        }

        /// <summary>
        /// Overrides the GeneratedStamp to put the comment tags in front.
        /// </summary>
        public override string GeneratedStamp
        {
            get { return @"<!-- " + base.GeneratedStamp + " -->"; }
        }

        /// <summary>
        /// Generate all of the code.
        /// </summary>
        /// <returns></returns>
        public override string Generate()
        {
            var generatedCode = new StringBuilder(GenerateTargetGo());
            generatedCode.Append(GenerateBuildOnly());
            generatedCode.Append(GenerateProjectTargets());
            generatedCode.Append(GenerateOnStart());
            generatedCode.Append(GenerateClearTests());
            generatedCode.Append(GenerateShowProgress());
            generatedCode.Append(GenerateSuccessFailure());

            // The xml version must be the first thing in the file.
            var returnCode = new StringBuilder(@"<?xml version=""1.0""?>");
            returnCode.Append(base.Generate());
            returnCode.Append(WrapInProjectElement(generatedCode.ToString(), "masterBuild"));
            return returnCode.ToString();
        }


        /// <summary>
        /// Generates the comments section for the file
        /// </summary>
        /// <returns></returns>
        public override string GenerateFileHeaderComments()
        {
            return string.Format(@"
<!-- {0}
-->
"
                                 , Strings.SplitMultiLineString(FilePurpose, " ")
                                 , DateTime.Now);
        }

        /// <summary>
        /// Initialize all delegates for code generation. 
        /// i.e. set the strategy pattern handlers.
        /// </summary>
        protected override void InitializeDelegates()
        {
            // nothing to initialize for the master build file generator.
        }

        #endregion


    }
}